Gridded Datasets II#
import numpy as np
import xarray as xr
import holoviews as hv
import geoviews as gv
import geoviews.feature as gf
from geoviews import opts
from cartopy import crs as ccrs
gv.extension('matplotlib', 'bokeh')
gv.output(size=200)
The main strength of HoloViews and its extensions (like GeoViews) is the ability to quickly explore complex datasets by declaring lower-dimensional views into a higher-dimensional space. In HoloViews we refer to the interface that allows you to do this as the conversion API. To begin with we will load a multi-dimensional dataset of surface temperatures for different “realizations” (modelling parameters) using XArray:
xr_ensembles = xr.open_dataset('../data/ensembles.nc')
xr_ensembles
<xarray.Dataset>
Dimensions: (realization: 13, time: 6, latitude: 145,
longitude: 192, bnds: 2)
Coordinates:
* realization (realization) int32 0 1 2 3 4 5 7 8 9 10 11 12 13
* time (time) datetime64[ns] 2011-08-16T12:00:00 ... 20...
* latitude (latitude) float32 -90.0 -88.75 ... 88.75 90.0
* longitude (longitude) float32 0.0 1.875 3.75 ... 356.2 358.1
forecast_reference_time (realization, time) datetime64[ns] ...
Dimensions without coordinates: bnds
Data variables:
surface_temperature (realization, time, latitude, longitude) float32 ...
latitude_longitude int32 ...
time_bnds (time, bnds) datetime64[ns] ...
Attributes:
source: Data from Met Office Unified Model
um_version: 7.6
Conventions: CF-1.5As we saw in the Gridded Datasets I Tutorial we can easily wrap this xarray data structure as a HoloViews Dataset:
dataset = gv.Dataset(xr_ensembles, vdims='surface_temperature')
dataset
:Dataset [realization,time,latitude,longitude] (surface_temperature)
From the repr we can immediately see the list of key dimensions (time, realization, longitude and latitude) and the value dimension of the cube (surface_temperature). However, unlike most other HoloViews Elements, the Dataset Element does not display itself visually. This is because it can be n-dimensional and therefore does not have any specific straightforward visual representation on a 2D display. To view the cube, we first have to transform it into individually visualizable chunks. Before doing so, we will want to supply a custom value formatter for the time dimension so that it is readable by humans:
hv.Dimension.type_formatters[np.datetime64] = '%Y-%m-%d'
Conversions#
A HoloViews Dataset is a wrapper around a complex multi-dimensional datastructure which allows the user to convert their data into individually visualizable views, each usually of lower dimensionality. This is done by grouping the data by some dimension and then casting it to a specific Element type, which visualizes itself.
The dataset.to interface makes this especially easy. To use it, you supply the Element type that you want to view the data as and the key dimensions of that view and it will figure out the rest. Depending on the type of Element, you can specify one or more dimensions to be displayed. GeoViews provides a set of GeoElements that allow you to display geographic data on a cartographic projection, but you can use any Elements from HoloViews for non-geographic plots.
Recall that the cube we are working with has 4 coordinate dimensions (or key dimensions as they are known in HoloViews) – time, realization, longitude, and latitude. For our purposes, a geographic plot is defined as a plot that has longitude along the x axis and latitude along the y axis. To declare a two-dimensional geographic plot, we therefore simply request a gv.Image plot with longitude and latitude as key dimensions. There is one value dimension (vdim) available, surface_temperature, and any remaining key dimensions (time and realization in this case) are assigned to a HoloMap data structure by default. The resulting HoloMap gives you widgets automatically to allow you to explore the data across the two “remaining” key dimensions (those not mapped onto axes of the image):
geo_dims = ['longitude', 'latitude']
(dataset.to(gv.Image, geo_dims) * gf.coastline)[::5, ::5]
In this way we can visualize the geographic data in a number of ways, currently either as an Image (as above) or as LineContours, FilledContours, or Points:
layout = hv.Layout([dataset.to(el, geo_dims)[::10, ::5] * gf.coastline
for el in [gv.FilledContours, gv.LineContours, gv.Points]]).cols(1)
layout.opts(opts.Points(color='surface_temperature', cmap='jet'))
Note that by default the conversion interface will automatically expand all the individual Elements, which can take some time if the data is very large. Instead we can also request the objects to be expanded dynamically using the dynamic keyword:
dataset.to(gv.Image, geo_dims, dynamic=True) * gf.coastline
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/IPython/core/formatters.py:974, in MimeBundleFormatter.__call__(self, obj, include, exclude)
971 method = get_real_method(obj, self.print_method)
973 if method is not None:
--> 974 return method(include=include, exclude=exclude)
975 return None
976 else:
File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/core/dimension.py:1286, in Dimensioned._repr_mimebundle_(self, include, exclude)
1279 def _repr_mimebundle_(self, include=None, exclude=None):
1280 """
1281 Resolves the class hierarchy for the class rendering the
1282 object using any display hooks registered on Store.display
1283 hooks. The output of all registered display_hooks is then
1284 combined and returned.
1285 """
-> 1286 return Store.render(self)
File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/core/options.py:1428, in Store.render(cls, obj)
1426 data, metadata = {}, {}
1427 for hook in hooks:
-> 1428 ret = hook(obj)
1429 if ret is None:
1430 continue
File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/ipython/display_hooks.py:287, in pprint_display(obj)
285 if not ip.display_formatter.formatters['text/plain'].pprint:
286 return None
--> 287 return display(obj, raw_output=True)
File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/ipython/display_hooks.py:261, in display(obj, raw_output, **kwargs)
259 elif isinstance(obj, (HoloMap, DynamicMap)):
260 with option_state(obj):
--> 261 output = map_display(obj)
262 elif isinstance(obj, Plot):
263 output = render(obj)
File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/ipython/display_hooks.py:149, in display_hook.<locals>.wrapped(element)
147 try:
148 max_frames = OutputSettings.options['max_frames']
--> 149 mimebundle = fn(element, max_frames=max_frames)
150 if mimebundle is None:
151 return {}, {}
File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/ipython/display_hooks.py:209, in map_display(vmap, max_frames)
206 max_frame_warning(max_frames)
207 return None
--> 209 return render(vmap)
File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/ipython/display_hooks.py:76, in render(obj, **kwargs)
73 if renderer.fig == 'pdf':
74 renderer = renderer.instance(fig='png')
---> 76 return renderer.components(obj, **kwargs)
File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/plotting/renderer.py:396, in Renderer.components(self, obj, fmt, comm, **kwargs)
393 embed = (not (dynamic or streams or self.widget_mode == 'live') or config.embed)
395 if embed or config.comms == 'default':
--> 396 return self._render_panel(plot, embed, comm)
397 return self._render_ipywidget(plot)
File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/plotting/renderer.py:403, in Renderer._render_panel(self, plot, embed, comm)
401 doc = Document()
402 with config.set(embed=embed):
--> 403 model = plot.layout._render_model(doc, comm)
404 if embed:
405 return render_model(model, comm)
File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/panel/viewable.py:738, in Viewable._render_model(self, doc, comm)
736 if comm is None:
737 comm = state._comm_manager.get_server_comm()
--> 738 model = self.get_root(doc, comm)
740 if self._design and self._design.theme.bokeh_theme:
741 doc.theme = self._design.theme.bokeh_theme
File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/panel/layout/base.py:307, in Panel.get_root(self, doc, comm, preprocess)
303 def get_root(
304 self, doc: Optional[Document] = None, comm: Optional[Comm] = None,
305 preprocess: bool = True
306 ) -> Model:
--> 307 root = super().get_root(doc, comm, preprocess)
308 # ALERT: Find a better way to handle this
309 if hasattr(root, 'styles') and 'overflow-x' in root.styles:
File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/panel/viewable.py:660, in Renderable.get_root(self, doc, comm, preprocess)
658 wrapper = self._design._wrapper(self)
659 if wrapper is self:
--> 660 root = self._get_model(doc, comm=comm)
661 if preprocess:
662 self._preprocess(root)
File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/panel/layout/base.py:175, in Panel._get_model(self, doc, root, parent, comm)
173 root = root or model
174 self._models[root.ref['id']] = (model, parent)
--> 175 objects, _ = self._get_objects(model, [], doc, root, comm)
176 props = self._get_properties(doc)
177 props[self._property_mapping['objects']] = objects
File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/panel/layout/base.py:157, in Panel._get_objects(self, model, old_objects, doc, root, comm)
155 else:
156 try:
--> 157 child = pane._get_model(doc, root, model, comm)
158 except RerenderError as e:
159 if e.layout is not None and e.layout is not self:
File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/panel/pane/holoviews.py:453, in HoloViews._get_model(self, doc, root, parent, comm)
451 del kwargs['height']
452 child_pane = self._get_pane(backend, state, **kwargs)
--> 453 self._update_plot(plot, child_pane)
454 model = child_pane._get_model(doc, root, parent, comm)
455 if ref in self._plots:
File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/panel/pane/holoviews.py:328, in HoloViews._update_plot(self, plot, pane)
326 plot.update(key)
327 else:
--> 328 plot.update(key)
329 if hasattr(plot.renderer, 'get_plot_state'):
330 pane.object = plot.renderer.get_plot_state(plot)
File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/plotting/mpl/plot.py:274, in MPLPlot.update(self, key)
272 if len(self) == 1 and key in (0, self.keys[0]) and not self.drawn:
273 return self.initialize_plot()
--> 274 return self.__getitem__(key)
File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/plotting/plot.py:443, in DimensionedPlot.__getitem__(self, frame)
441 if not isinstance(frame, tuple):
442 frame = self.keys[frame]
--> 443 self.update_frame(frame)
444 return self.state
File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/plotting/mpl/plot.py:62, in mpl_rc_context.<locals>.wrapper(self, *args, **kwargs)
60 def wrapper(self, *args, **kwargs):
61 with _rc_context(self.fig_rcparams):
---> 62 return f(self, *args, **kwargs)
File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/plotting/mpl/element.py:1246, in OverlayPlot.update_frame(self, key, ranges, element)
1243 self._create_dynamic_subplots(key, items, ranges)
1245 # Update plot options
-> 1246 plot_opts = self.lookup_options(element, 'plot').options
1247 inherited = self._traverse_options(element, 'plot',
1248 self._propagate_options,
1249 defaults=False)
1250 plot_opts.update(**{k: v[0] for k, v in inherited.items()
1251 if k not in plot_opts})
File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/plotting/plot.py:288, in Plot.lookup_options(cls, obj, group)
286 @classmethod
287 def lookup_options(cls, obj, group):
--> 288 return lookup_options(obj, group, cls.backend)
File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/core/options.py:96, in lookup_options(obj, group, backend)
93 except SkipRendering:
94 style_opts = None
---> 96 node = Store.lookup_options(backend, obj, group)
97 if group == 'style' and style_opts is not None:
98 return node.filtered(style_opts)
File /usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/core/options.py:1277, in Store.lookup_options(cls, backend, obj, group, defaults)
1274 @classmethod
1275 def lookup_options(cls, backend, obj, group, defaults=True):
1276 # Current custom_options dict may not have entry for obj.id
-> 1277 if obj.id in cls._custom_options[backend]:
1278 return cls._custom_options[backend][obj.id].closest(
1279 obj, group, defaults, backend=backend)
1280 elif not defaults:
AttributeError: 'NoneType' object has no attribute 'id'
:DynamicMap [realization,time]
:Overlay
.Image.I :Image [longitude,latitude] (surface_temperature)
.Coastline.I :Feature [Longitude,Latitude]
Using dynamic mode means that the data for each frame is only extracted when you’re actually viewing that part of the data, which can have huge benefits in terms of speed and memory consumption. However, it relies on having a running Python process to render and serve each image, and so it cannot be used when generating static HTML output such as for the GeoViews web site.
Irregularly sampled data#
Often gridded datasets are not regularly sampled, instead providing irregularly sampled multi-dimensional coordinates. Such datasets can be easily visualized using the QuadMesh element.
xrds = xr.tutorial.open_dataset('rasm').load()
qmesh = gv.Dataset(xrds.Tair).to(gv.QuadMesh, ['xc', 'yc'], dynamic=True).redim.range(Tair=(-30, 30))
qmesh.opts(colorbar=True, cmap='RdBu_r', projection=ccrs.Robinson()) * gf.coastline
Non-geographic views#
So far we have focused entirely on geographic views of the data, plotting the data on a projection. However the conversion interface is completely general, allowing us to slice and dice the data in any way we like. For these views we will switch to the bokeh plotting extension:
hv.output(backend='bokeh')
The simplest example of this capability is simply a view showing the temperature over time for each realization, longitude, and latitude coordinate:
curves = dataset.to(hv.Curve, 'time', dynamic=True).overlay('realization')
curves.opts(
opts.Curve(xrotation=25, width=600, height=400, framewise=True),
opts.NdOverlay(legend_position='right', toolbar='right'))
WARNING:param.outer_fn: Callable raised "ValueError('None is not in list')".
Invoked as outer_fn(None, None)
WARNING:param.dynamic_operation: Callable raised "ValueError('None is not in list')".
Invoked as dynamic_operation(None, None)
Traceback (most recent call last):
File "/usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/plotting/util.py", line 293, in get_plot_frame
return map_obj[key]
File "/usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/core/spaces.py", line 1216, in __getitem__
val = self._execute_callback(*tuple_key)
File "/usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/core/spaces.py", line 983, in _execute_callback
retval = self.callback(*args, **kwargs)
File "/usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/core/spaces.py", line 581, in __call__
ret = self.callable(*args, **kwargs)
File "/usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/util/__init__.py", line 1033, in dynamic_operation
key, obj = resolve(key, kwargs)
File "/usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/util/__init__.py", line 1022, in resolve
return key, map_obj[key]
File "/usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/core/spaces.py", line 1216, in __getitem__
val = self._execute_callback(*tuple_key)
File "/usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/core/spaces.py", line 983, in _execute_callback
retval = self.callback(*args, **kwargs)
File "/usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/core/spaces.py", line 581, in __call__
ret = self.callable(*args, **kwargs)
File "/usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/core/spaces.py", line 1559, in outer_fn
selected = HoloMap(self.select(**dict(dim_vals)))
File "/usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/core/spaces.py", line 1263, in select
selection = super().select(selection_specs=selection_specs, **kwargs)
File "/usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/core/dimension.py", line 1109, in select
selection = self.get(tuple(select), None)
File "/usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/core/ndmapping.py", line 541, in get
return self[key]
File "/usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/core/spaces.py", line 1205, in __getitem__
cache = super().__getitem__(key)
File "/usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/core/ndmapping.py", line 628, in __getitem__
conditions = self._generate_conditions(map_slice)
File "/usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/core/ndmapping.py", line 700, in _generate_conditions
dim_slice = [dim.values.index(dim_val)
File "/usr/share/miniconda3/envs/test-environment/lib/python3.9/site-packages/holoviews/core/ndmapping.py", line 700, in <listcomp>
dim_slice = [dim.values.index(dim_val)
ValueError: None is not in list
Note that the longitude slider will have no effect, if latitude is -90 or +90, since there is only one data point for the North or South poles (regardless of the declared longitude). Here the .overlay gives a different curve for each realization; without it all realization values would be pooled together.
We can also make non-geographic 2D plots, for instance as a HeatMap over time and realization, at a specified longitude and latitude:
dataset.to(hv.HeatMap, ['realization', 'time'], dynamic=True).opts(width=600, colorbar=True, tools=['hover'])
Lower-dimensional views#
So far all the conversions shown have incorporated each of the available coordinate dimensions explicitly. However, often times we want to see the spread of values along one or more dimensions, pooling all the other dimensions together. A simple example of this is a box plot where we might want to see the spread of surface_temperature on each day, pooled across all latitude and longitude coordinates. To pool across particular dimensions, we can explicitly declare the “map” dimensions, which are the key dimensions of the HoloMap container rather than those of the Elements contained in the HoloMap. By explicitly declaring no dimensions to groupby, we can tell the conversion interface to pool across all dimensions except the particular key dimension(s) supplied, in this case the 'time' (plot A) and 'realization' (plot B):
hv.output(backend='matplotlib')
hv.Layout([dataset.to(hv.Violin, d, groupby=[], datatype=['dataframe']).opts(xrotation=25)
for d in ['time', 'realization']])
Reducing the data#
So far all the examples we have seen have displayed all the data in some way or another. Another way to explore a dataset is to explicitly reduce the dimensionality or select subregions of a dataset. There are two main ways to do this—either we explicitly select a subset of the data, or we collapse a dimension using an aggregation function, e.g. by computing a mean along a particular dimension.
Selecting slices#
Using the select method we can easily select ranges of coordinates in the dataset. Unfortunately, the select method does not currently know that latitude and longitude are cyclic, so instead we have to select regions at both ends of the prime meridian (0$^\circ$ longitude) and overlay them. In this way we can stitch together multiple cubes or xarrays or simply view specific subregions:
northern = dataset.select(latitude=slice(25, 75))
(northern.select(longitude=slice(260, 305)).to(gv.Image, geo_dims) *
northern.select(longitude=slice(330, 362)).to(gv.Image, geo_dims) *
gf.coastline)[::5, ::5]
Selecting a particular coordinate#
To examine one particular coordinate, we can select it, cast the data to Curves, reindex the data to drop the now-constant latitude and longitude dimensions, and overlay the remaining ‘realization’ dimension:
hv.output(backend='bokeh')
curves = dataset.select(latitude=0, longitude=0).to(hv.Curve, ['time']).reindex().overlay()
curves.opts(width=600, height=400, legend_position='right', toolbar='above')
As you can see, with GeoViews and HoloViews it is very simple to select precisely which aspects of complex, multidimensional datasets that you want to focus on.